package ch.fhnw.ams.sudoku;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * Represents the Sudoku grid with 9x9 Fields.
 * 
 * @author amsler
 *
 */
public class SudokuGrid implements Iterable<SudokuField> {
	
	public final static int ROW_SIZE = 9; //left to right
	public final static int COLUMN_SIZE = 9; //top down
	public final static int NUM_OF_BLOCKS = 9;
	public final static int BLOCK_LENGTH = 3;
	
	private SudokuField sudokuField[][] = new SudokuField[ROW_SIZE][COLUMN_SIZE];
	
	
	
	/**
	 * Creates a new empty Grid
	 */
	public SudokuGrid() {
		for (int row = 0; row < ROW_SIZE; row++) {
			for (int column = 0; column < COLUMN_SIZE; column++) {
				sudokuField[row][column] = new SudokuField(row, column);
				sudokuField[row][column].setPuzzelNumber(false);
			}
		}
	}
	
	/**
	 * Creates a new grid with specified numbers
	 * 
	 * @param data
	 *            Sudoku data with the format <code>int[row][colum]</code>. Size
	 *            of row and column must be 9.
	 */
	public SudokuGrid(int[][] data) throws InvalidSudokuException {
		this();
		
		if (!isValidSudokuData(data)) {
			throw new InvalidSudokuException();
		}
		
		for (int row = 0; row < data.length; row++) {
			for (int column = 0; column < data[row].length; column++) {
				int number = data[row][column];
				
				if (number < -9 || number > 9) {
					throw new InvalidSudokuException("illegal number at row "
							+ row + " column " + column);
				}
				
				SudokuField field = getField(row, column);
				field.setNumber(Math.abs(number));
				if (number > 0) {
					field.setPuzzelNumber(true);
				} else {
					field.setPuzzelNumber(false);
				}
			}
		}
	}
	
	private boolean isValidSudokuData(int[][] data) {
		if (data == null || data.length != SudokuGrid.ROW_SIZE) {
			return false;
		}
		for (int row = 0; row < data.length; row++) {
			if (data[row].length != SudokuGrid.COLUMN_SIZE) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Sets a specific {@link SudokuField} in this grid
	 * 
	 * @param number
	 *            the number to set
	 * @param row
	 *            the row of the field
	 * @param column
	 *            the column of the field
	 * @param isPuzzelNumber
	 *            <code>true</code> if the number belongs to a puzzle
	 * @throws IllegalArgumentException
	 *             if the <code>number</code> is negative or greater than 9,
	 *             or the field defined by row and column doesn't exist
	 */
	public void setField(int number, int row, int column, boolean isPuzzelNumber) {
		if (row < 0 || row >= ROW_SIZE || column < 0 || column >= COLUMN_SIZE
				|| number < 0 || number > 9) {
			throw new IllegalArgumentException();
		}

		SudokuField field = getField(row, column);
		field.setNumber(number);
		field.setPuzzelNumber(isPuzzelNumber);
	}
	
	/**
	 * Gets a specific {@link SudokuField} from this grid.
	 * 
	 * @param row
	 *            the row of the field
	 * @param column
	 *            the column of the field
	 * @return the {@link SudokuField}
	 */
	public SudokuField getField(int row, int column) {
		if (row < 0 || row >= ROW_SIZE || column < 0 || column >= COLUMN_SIZE) {
			throw new IllegalArgumentException();
		}
		
		return sudokuField[row][column];
	}
	
	/**
	 * Deletes all numbers of this grid.
	 */
	public void clear() {
		for (SudokuField field : this) {
			field.clear();
			field.setPuzzelNumber(true);
		}
	}
	
	/**
	 * Deletes all numbers which are not part of a puzzle
	 */
	public void reset() {
		for (SudokuField field : this) {
			if (!field.isPuzzelNumber()) {
				field.clear();
			}
		}
	}
	
	/**
	 * Checks if the grid is completely filled in.
	 * 
	 * @return <code>true</code> if all fields contain a number
	 */
	public boolean isCompleted() {
		for (SudokuField field : this) {
			if (field.isEmpty()) {
				return false;
			}
		}
		
		return true;
	}
	
	/**
	 * Checks the sudoku rules.
	 * 
	 * <Ul>
	 * <li>A number can appear only once on each column</li>
	 * <li>A number can appear only once on each row</li>
	 * <li>A number can appear only once on each region</li>
	 * </ul>
	 * 
	 * @return <code>true</code> if this grid is a valid Sudoku
	 */
	public boolean checkRules() {
		//reset incorrect fields
		for (SudokuField field : this) {
			field.setIncorrect(false);
		}
		
		List<SudokuField> sudokuFields = getAllIncorrectFields();
		
		for (SudokuField sudokuField : sudokuFields) {
			sudokuField.setIncorrect(true);
		}
		
		return sudokuFields.isEmpty();
	}
	
	/**
	 * Gets all fields which break one of the following rules: 
	 * <Ul>
	 * <li>A number can appear only once on each column</li>
	 * <li>A number can appear only once on each row</li>
	 * <li>A number can appear only once on each region</li>
	 * </ul>
	 * 
	 * @return a list with incorrect fields
	 */
	public List<SudokuField> getAllIncorrectFields() {
		List<SudokuField> fieldsHarmingRule = new ArrayList<SudokuField>();
		
		for (SudokuField field : checkAllRows()) {
			if (!fieldsHarmingRule.contains(field)) {
				fieldsHarmingRule.add(field);
			}
		}
		
		for (SudokuField field : checkAllColumns()) {
			if (!fieldsHarmingRule.contains(field)) {
				fieldsHarmingRule.add(field);
			}
		}
		
		for (SudokuField field : checkAllBlocks()) {
			if (!fieldsHarmingRule.contains(field)) {
				fieldsHarmingRule.add(field);
			}
		}
		
		return fieldsHarmingRule;
	}
	
	private List<SudokuField> checkAllRows() {
		List<SudokuField> fieldsHarmingRule = new ArrayList<SudokuField>();
		
		for (int i = 0; i < ROW_SIZE; i++) {
			SudokuField[] row = getRowAt(i);
			fieldsHarmingRule.addAll(getDuplicatedFields(row));
		}
		
		return fieldsHarmingRule;
	}
	
	private List<SudokuField> checkAllColumns() {
		List<SudokuField> fieldsHarmingRule = new ArrayList<SudokuField>();
		
		for (int i = 0; i < COLUMN_SIZE; i++) {
			SudokuField[] column = getColumnAt(i);
			fieldsHarmingRule.addAll(getDuplicatedFields(column));
		}
		
		return fieldsHarmingRule;
	}
	
	private List<SudokuField> checkAllBlocks() {
		List<SudokuField> fieldsHarmingRule = new ArrayList<SudokuField>();
		
		for (int i = 0; i < NUM_OF_BLOCKS; i++) {
			SudokuField[] block = getBlockAt(i);
			fieldsHarmingRule.addAll(getDuplicatedFields(block));
		}
		
		return fieldsHarmingRule;
	}
	
	private SudokuField[] getRowAt(int rowPosition) {
		SudokuField[] row = new SudokuField[COLUMN_SIZE];
		
		for (int column = 0; column < COLUMN_SIZE; column++) {
			row[column] = getField(rowPosition, column);
		}
		
		return row;
	}
	
	private SudokuField[] getColumnAt(int columnPosition) {
		SudokuField[] column = new SudokuField[ROW_SIZE];
		
		for (int row = 0; row < ROW_SIZE; row++) {
			column[row] = getField(row, columnPosition);
		}
		
		return column;
	}
	
	private SudokuField[] getBlockAt(int blockPosition) {
		SudokuField[] block = new SudokuField[(BLOCK_LENGTH * BLOCK_LENGTH)];
		
		int rowStart = (blockPosition / BLOCK_LENGTH) * BLOCK_LENGTH;
		int columnStart = (blockPosition % BLOCK_LENGTH) * BLOCK_LENGTH;
		
		int blockCount = 0;
		for (int row = rowStart; row < (rowStart + 3); row++) {
			for (int column = columnStart; column < (columnStart + 3); column++) {
				block[blockCount] = getField(row, column);
				blockCount++;
			}
		}
		
		return block;
	}
	
	private List<SudokuField> getDuplicatedFields(SudokuField[] fields) {
		List<SudokuField> duplicatedFields = new ArrayList<SudokuField>();
		
		for (SudokuField outerField : fields) {
			for (SudokuField innerField : fields) {
				if (outerField.getNumber() == innerField.getNumber() 
						&& outerField != innerField
						&& !outerField.isEmpty() && !innerField.isEmpty()) {
					
					if (!duplicatedFields.contains(innerField)) {
						duplicatedFields.add(innerField);
					}
					if (!duplicatedFields.contains(outerField)) {
						duplicatedFields.add(outerField);
					}
				}
			}
		}
		
		return duplicatedFields;
	}
	
	/**
	 * Returns an array containing all numbers in this grid.
	 * 
	 * @param signedPuzzelNumbers
	 *            if <code>true</code> the solution numbers are negative
	 * @return Sudoku data with the format <code>int[row][colum]</code>. Size of
	 *         row and column must be 9.
	 */
	public int[][] asArray(boolean signedNumbers) {
		int[][] array = new int[ROW_SIZE][COLUMN_SIZE];
		
		for (int row = 0; row < ROW_SIZE; row++) {
			for (int column = 0; column < COLUMN_SIZE; column++) {
				SudokuField field = getField(row, column);
				int number = field.getNumber();
				if (signedNumbers && !field.isPuzzelNumber()) {
					number *= -1;
				}
				array[row][column] = number;
			}
		}
		
		return array;
	}
	
	/**
	 * Sets the solution found by a {@link ISolver}.
	 * 
	 * @param solution
	 *            Sudoku data with the format <code>int[row][colum]</code>. Size
	 *            of row and column must be 9.
	 * @throws InvalidSudokuException
	 *             if the Sudoku data array has an invalid format
	 */
	public void setSolution(int[][] solution) throws InvalidSudokuException {
		if (!isValidSudokuData(solution)) {
			throw new InvalidSudokuException();
		}
		
		for (int row = 0; row < ROW_SIZE; row++) {
			for (int column = 0; column < COLUMN_SIZE; column++) {
				SudokuField field = getField(row, column);
				if (field.isEmpty()){
					field.setNumber(solution[row][column]);
					field.setGenerated(true);
				}
			}
		}
	}
	
	/**
	 * @return <code>true</code> if this grid is blank
	 */
	public boolean isEmpty() {
		for (SudokuField field : this) {
			if (!field.isEmpty()) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Returns the amount of puzzle numbers in this grid.
	 * 
	 * @return amount of puzzle numbers
	 */
	public int getPuzzleNumberCount() {
		int count = 0;
		for (SudokuField field : this) {
			if (!field.isEmpty() && field.isPuzzelNumber()) {
				count++;
			}
		}
		return count;
	}
	
	/**
	 * Returns the amount of solution numbers in this grid.
	 * 
	 * @return amount of solution numbers
	 */
	public int getSolutionNumberCount() {
		int count = 0;
		for (SudokuField field : this) {
			if (!field.isEmpty() && !field.isPuzzelNumber()) {
				count++;
			}
		}
		return count;
	}
	
	@Override
	public Iterator<SudokuField> iterator() {
		return new SudokuGridIterator(this);
	}
	
	@Override 
	public String toString() {
		return SudokuUtil.toSudokuString(asArray(true));
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Arrays.hashCode(sudokuField);
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		SudokuGrid other = (SudokuGrid) obj;
		if (!Arrays.deepEquals(sudokuField, other.sudokuField))
			return false;
		return true;
	}

	
	
	/**
	 * Iterator implementation to iterate over the complete {@link SudokuGrid}. 
	 */
	public static class SudokuGridIterator implements Iterator<SudokuField> {
		
		private final SudokuGrid sudokuGrid;
		private int rowCount;
		private int columnCount;
		
		public SudokuGridIterator(SudokuGrid sudokuGrid) {
			this.sudokuGrid = sudokuGrid;
		}

		@Override
		public boolean hasNext() {
			if (rowCount < ROW_SIZE && columnCount < COLUMN_SIZE) {
				return true;
			}
			return false;
		}

		@Override
		public SudokuField next() {
			if (rowCount > ROW_SIZE || columnCount > COLUMN_SIZE) {
				throw new IllegalArgumentException("index out of bounds");
			}
			
			SudokuField field = sudokuGrid.getField(rowCount, columnCount);
			
			//next iteration
			if (columnCount == 8) {
				rowCount++;
				columnCount = 0;
			} else {
				columnCount++;
			}
			
			return field;
		}
		
		/**
		 * do not invoke this method
		 */
		@Override
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
	}
}




